/**
 * Copyright (c) 2006-2015, JGraph Ltd
 * Copyright (c) 2006-2015, Gaudenz Alder
 */
/**
 * Class: mxArrowConnector
 *
 * Extends <mxShape> to implement an new rounded arrow shape with support for
 * waypoints and double arrows. (The shape is used to represent edges, not
 * vertices.) This shape is registered under <mxConstants.SHAPE_ARROW_CONNECTOR>
 * in <mxCellRenderer>.
 *
 * Constructor: mxArrowConnector
 *
 * Constructs a new arrow shape.
 *
 * Parameters:
 *
 * points - Array of <mxPoints> that define the points. This is stored in
 * <mxShape.points>.
 * fill - String that defines the fill color. This is stored in <fill>.
 * stroke - String that defines the stroke color. This is stored in <stroke>.
 * strokewidth - Optional integer that defines the stroke width. Default is
 * 1. This is stored in <strokewidth>.
 * arrowWidth - Optional integer that defines the arrow width. Default is
 * <mxConstants.ARROW_WIDTH>. This is stored in <arrowWidth>.
 * spacing - Optional integer that defines the spacing between the arrow shape
 * and its endpoints. Default is <mxConstants.ARROW_SPACING>. This is stored in
 * <spacing>.
 * endSize - Optional integer that defines the size of the arrowhead. Default
 * is <mxConstants.ARROW_SIZE>. This is stored in <endSize>.
 */
function mxArrowConnector(
  points,
  fill,
  stroke,
  strokewidth,
  arrowWidth,
  spacing,
  endSize
) {
  mxShape.call(this);
  this.points = points;
  this.fill = fill;
  this.stroke = stroke;
  this.strokewidth = strokewidth != null ? strokewidth : 1;
  this.arrowWidth = arrowWidth != null ? arrowWidth : mxConstants.ARROW_WIDTH;
  this.arrowSpacing = spacing != null ? spacing : mxConstants.ARROW_SPACING;
  this.startSize = mxConstants.ARROW_SIZE / 5;
  this.endSize = mxConstants.ARROW_SIZE / 5;
}

/**
 * Extends mxShape.
 */
mxUtils.extend(mxArrowConnector, mxShape);

/**
 * Variable: useSvgBoundingBox
 *
 * Allows to use the SVG bounding box in SVG. Default is false for performance
 * reasons.
 */
mxArrowConnector.prototype.useSvgBoundingBox = true;

/**
 * Variable: resetStyles
 *
 * Overrides mxShape to reset spacing.
 */
mxArrowConnector.prototype.resetStyles = function () {
  mxShape.prototype.resetStyles.apply(this, arguments);

  this.arrowSpacing = mxConstants.ARROW_SPACING;
};

/**
 * Overrides apply to get smooth transition from default start- and endsize.
 */
mxArrowConnector.prototype.apply = function (state) {
  mxShape.prototype.apply.apply(this, arguments);

  if (this.style != null) {
    this.startSize =
      mxUtils.getNumber(
        this.style,
        mxConstants.STYLE_STARTSIZE,
        mxConstants.ARROW_SIZE / 5
      ) * 3;
    this.endSize =
      mxUtils.getNumber(
        this.style,
        mxConstants.STYLE_ENDSIZE,
        mxConstants.ARROW_SIZE / 5
      ) * 3;
  }
};

/**
 * Function: augmentBoundingBox
 *
 * Augments the bounding box with the edge width and markers.
 */
mxArrowConnector.prototype.augmentBoundingBox = function (bbox) {
  mxShape.prototype.augmentBoundingBox.apply(this, arguments);

  var w = this.getEdgeWidth();

  if (this.isMarkerStart()) {
    w = Math.max(w, this.getStartArrowWidth());
  }

  if (this.isMarkerEnd()) {
    w = Math.max(w, this.getEndArrowWidth());
  }

  bbox.grow((w / 2 + this.strokewidth) * this.scale);
};

/**
 * Function: paintEdgeShape
 *
 * Paints the line shape.
 */
mxArrowConnector.prototype.paintEdgeShape = function (c, pts) {
  // Geometry of arrow
  var strokeWidth = this.strokewidth;

  if (this.outline) {
    strokeWidth = Math.max(
      1,
      mxUtils.getNumber(
        this.style,
        mxConstants.STYLE_STROKEWIDTH,
        this.strokewidth
      )
    );
  }

  var startWidth = this.getStartArrowWidth() + strokeWidth;
  var endWidth = this.getEndArrowWidth() + strokeWidth;
  var edgeWidth = this.outline
    ? this.getEdgeWidth() + strokeWidth
    : this.getEdgeWidth();
  var openEnded = this.isOpenEnded();
  var markerStart = this.isMarkerStart();
  var markerEnd = this.isMarkerEnd();
  var spacing = openEnded ? 0 : this.arrowSpacing + strokeWidth / 2;
  var startSize = this.startSize + strokeWidth;
  var endSize = this.endSize + strokeWidth;
  var isRounded = this.isArrowRounded();

  // Base vector (between first points)
  var pe = pts[pts.length - 1];

  // Finds first non-overlapping point
  var i0 = 1;

  while (
    i0 < pts.length - 1 &&
    pts[i0].x == pts[0].x &&
    pts[i0].y == pts[0].y
  ) {
    i0++;
  }

  var dx = pts[i0].x - pts[0].x;
  var dy = pts[i0].y - pts[0].y;
  var dist = Math.sqrt(dx * dx + dy * dy);

  if (dist == 0) {
    return;
  }

  // Computes the norm and the inverse norm
  var nx = dx / dist;
  var nx2,
    nx1 = nx;
  var ny = dy / dist;
  var ny2,
    ny1 = ny;
  var orthx = edgeWidth * ny;
  var orthy = -edgeWidth * nx;

  // Stores the inbound function calls in reverse order in fns
  var fns = [];

  if (isRounded) {
    c.setLineJoin("round");
  } else if (pts.length > 2) {
    // Only mitre if there are waypoints
    c.setMiterLimit(1.42);
  }

  c.begin();

  var startNx = nx;
  var startNy = ny;

  if (markerStart && !openEnded) {
    this.paintMarker(
      c,
      pts[0].x,
      pts[0].y,
      nx,
      ny,
      startSize,
      startWidth,
      edgeWidth,
      spacing,
      true
    );
  } else {
    var outStartX = pts[0].x + orthx / 2 + spacing * nx;
    var outStartY = pts[0].y + orthy / 2 + spacing * ny;
    var inEndX = pts[0].x - orthx / 2 + spacing * nx;
    var inEndY = pts[0].y - orthy / 2 + spacing * ny;

    if (openEnded) {
      c.moveTo(outStartX, outStartY);

      fns.push(function () {
        c.lineTo(inEndX, inEndY);
      });
    } else {
      c.moveTo(inEndX, inEndY);
      c.lineTo(outStartX, outStartY);
    }
  }

  var dx1 = 0;
  var dy1 = 0;
  var dist1 = 0;

  for (var i = 0; i < pts.length - 2; i++) {
    // Work out in which direction the line is bending
    var pos = mxUtils.relativeCcw(
      pts[i].x,
      pts[i].y,
      pts[i + 1].x,
      pts[i + 1].y,
      pts[i + 2].x,
      pts[i + 2].y
    );

    dx1 = pts[i + 2].x - pts[i + 1].x;
    dy1 = pts[i + 2].y - pts[i + 1].y;

    dist1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);

    if (dist1 != 0) {
      nx1 = dx1 / dist1;
      ny1 = dy1 / dist1;

      var tmp1 = nx * nx1 + ny * ny1;
      tmp = Math.max(Math.sqrt((tmp1 + 1) / 2), 0.04);

      // Work out the normal orthogonal to the line through the control point and the edge sides intersection
      nx2 = nx + nx1;
      ny2 = ny + ny1;

      var dist2 = Math.sqrt(nx2 * nx2 + ny2 * ny2);

      if (dist2 != 0) {
        nx2 = nx2 / dist2;
        ny2 = ny2 / dist2;

        // Higher strokewidths require a larger minimum bend, 0.35 covers all but the most extreme cases
        var strokeWidthFactor = Math.max(
          tmp,
          Math.min(this.strokewidth / 200 + 0.04, 0.35)
        );
        var angleFactor =
          pos != 0 && isRounded
            ? Math.max(0.1, strokeWidthFactor)
            : Math.max(tmp, 0.06);

        var outX = pts[i + 1].x + (ny2 * edgeWidth) / 2 / angleFactor;
        var outY = pts[i + 1].y - (nx2 * edgeWidth) / 2 / angleFactor;
        var inX = pts[i + 1].x - (ny2 * edgeWidth) / 2 / angleFactor;
        var inY = pts[i + 1].y + (nx2 * edgeWidth) / 2 / angleFactor;

        if (pos == 0 || !isRounded) {
          // If the two segments are aligned, or if we're not drawing curved sections between segments
          // just draw straight to the intersection point
          c.lineTo(outX, outY);

          (function (x, y) {
            fns.push(function () {
              c.lineTo(x, y);
            });
          })(inX, inY);
        } else if (pos == -1) {
          var c1x = inX + ny * edgeWidth;
          var c1y = inY - nx * edgeWidth;
          var c2x = inX + ny1 * edgeWidth;
          var c2y = inY - nx1 * edgeWidth;
          c.lineTo(c1x, c1y);
          c.quadTo(outX, outY, c2x, c2y);

          (function (x, y) {
            fns.push(function () {
              c.lineTo(x, y);
            });
          })(inX, inY);
        } else {
          c.lineTo(outX, outY);

          (function (x, y) {
            var c1x = outX - ny * edgeWidth;
            var c1y = outY + nx * edgeWidth;
            var c2x = outX - ny1 * edgeWidth;
            var c2y = outY + nx1 * edgeWidth;

            fns.push(function () {
              c.quadTo(x, y, c1x, c1y);
            });
            fns.push(function () {
              c.lineTo(c2x, c2y);
            });
          })(inX, inY);
        }

        nx = nx1;
        ny = ny1;
      }
    }
  }

  orthx = edgeWidth * ny1;
  orthy = -edgeWidth * nx1;

  if (markerEnd && !openEnded) {
    this.paintMarker(
      c,
      pe.x,
      pe.y,
      -nx,
      -ny,
      endSize,
      endWidth,
      edgeWidth,
      spacing,
      false
    );
  } else {
    c.lineTo(
      pe.x - spacing * nx1 + orthx / 2,
      pe.y - spacing * ny1 + orthy / 2
    );

    var inStartX = pe.x - spacing * nx1 - orthx / 2;
    var inStartY = pe.y - spacing * ny1 - orthy / 2;

    if (!openEnded) {
      c.lineTo(inStartX, inStartY);
    } else {
      c.moveTo(inStartX, inStartY);

      fns.splice(0, 0, function () {
        c.moveTo(inStartX, inStartY);
      });
    }
  }

  for (var i = fns.length - 1; i >= 0; i--) {
    fns[i]();
  }

  if (openEnded) {
    c.end();
    c.stroke();
  } else {
    c.close();
    c.fillAndStroke();
  }

  // Workaround for shadow on top of base arrow
  c.setShadow(false);

  // Need to redraw the markers without the low miter limit
  c.setMiterLimit(4);

  if (isRounded) {
    c.setLineJoin("flat");
  }

  if (pts.length > 2) {
    // Only to repaint markers if no waypoints
    // Need to redraw the markers without the low miter limit
    c.setMiterLimit(4);
    if (markerStart && !openEnded) {
      c.begin();
      this.paintMarker(
        c,
        pts[0].x,
        pts[0].y,
        startNx,
        startNy,
        startSize,
        startWidth,
        edgeWidth,
        spacing,
        true
      );
      c.stroke();
      c.end();
    }

    if (markerEnd && !openEnded) {
      c.begin();
      this.paintMarker(
        c,
        pe.x,
        pe.y,
        -nx,
        -ny,
        endSize,
        endWidth,
        edgeWidth,
        spacing,
        true
      );
      c.stroke();
      c.end();
    }
  }
};

/**
 * Function: paintEdgeShape
 *
 * Paints the line shape.
 */
mxArrowConnector.prototype.paintMarker = function (
  c,
  ptX,
  ptY,
  nx,
  ny,
  size,
  arrowWidth,
  edgeWidth,
  spacing,
  initialMove
) {
  var widthArrowRatio = edgeWidth / arrowWidth;
  var orthx = (edgeWidth * ny) / 2;
  var orthy = (-edgeWidth * nx) / 2;

  var spaceX = (spacing + size) * nx;
  var spaceY = (spacing + size) * ny;

  if (initialMove) {
    c.moveTo(ptX - orthx + spaceX, ptY - orthy + spaceY);
  } else {
    c.lineTo(ptX - orthx + spaceX, ptY - orthy + spaceY);
  }

  c.lineTo(
    ptX - orthx / widthArrowRatio + spaceX,
    ptY - orthy / widthArrowRatio + spaceY
  );
  c.lineTo(ptX + spacing * nx, ptY + spacing * ny);
  c.lineTo(
    ptX + orthx / widthArrowRatio + spaceX,
    ptY + orthy / widthArrowRatio + spaceY
  );
  c.lineTo(ptX + orthx + spaceX, ptY + orthy + spaceY);
};

/**
 * Function: isArrowRounded
 *
 * Returns wether the arrow is rounded
 */
mxArrowConnector.prototype.isArrowRounded = function () {
  return this.isRounded;
};

/**
 * Function: getStartArrowWidth
 *
 * Returns the width of the start arrow
 */
mxArrowConnector.prototype.getStartArrowWidth = function () {
  return mxConstants.ARROW_WIDTH;
};

/**
 * Function: getEndArrowWidth
 *
 * Returns the width of the end arrow
 */
mxArrowConnector.prototype.getEndArrowWidth = function () {
  return mxConstants.ARROW_WIDTH;
};

/**
 * Function: getEdgeWidth
 *
 * Returns the width of the body of the edge
 */
mxArrowConnector.prototype.getEdgeWidth = function () {
  return mxConstants.ARROW_WIDTH / 3;
};

/**
 * Function: isOpenEnded
 *
 * Returns whether the ends of the shape are drawn
 */
mxArrowConnector.prototype.isOpenEnded = function () {
  return false;
};

/**
 * Function: isMarkerStart
 *
 * Returns whether the start marker is drawn
 */
mxArrowConnector.prototype.isMarkerStart = function () {
  return (
    mxUtils.getValue(
      this.style,
      mxConstants.STYLE_STARTARROW,
      mxConstants.NONE
    ) != mxConstants.NONE
  );
};

/**
 * Function: isMarkerEnd
 *
 * Returns whether the end marker is drawn
 */
mxArrowConnector.prototype.isMarkerEnd = function () {
  return (
    mxUtils.getValue(
      this.style,
      mxConstants.STYLE_ENDARROW,
      mxConstants.NONE
    ) != mxConstants.NONE
  );
};
